Відкрийте для себе потужність зіставлення зі зразком у JavaScript. Дізнайтеся, як ця концепція функціонального програмування покращує оператори switch для чистого, більш декларативного та надійного коду.
Сила елегантності: глибоке занурення в зіставлення зі зразком у JavaScript
Десятиліттями розробники JavaScript покладалися на звичний набір інструментів для умовної логіки: шанований ланцюжок if/else та класичний оператор switch. Вони є робочими конячками логіки розгалуження, функціональними та передбачуваними. Проте, оскільки наші додатки зростають у складності, і ми приймаємо такі парадигми, як функціональне програмування, обмеження цих інструментів стають все більш очевидними. Довгі ланцюжки if/else можуть стати важкими для читання, а оператори switch, з їхніми простими перевірками на рівність та особливостями "провалювання" (fall-through), часто не справляються при роботі зі складними структурами даних.
Зустрічайте зіставлення зі зразком (Pattern Matching). Це не просто "оператор switch на стероїдах"; це зміна парадигми. Походячи з функціональних мов, таких як Haskell, ML та Rust, зіставлення зі зразком — це механізм перевірки значення на відповідність серії зразків. Він дозволяє вам деструктурувати складні дані, перевіряти їхню форму та виконувати код на основі цієї структури — все це в одній виразній конструкції. Це перехід від імперативної перевірки ("як перевірити значення") до декларативного зіставлення ("як виглядає значення").
Ця стаття є вичерпним посібником для розуміння та використання зіставлення зі зразком у сучасному JavaScript. Ми розглянемо його основні концепції, практичне застосування та те, як ви можете використовувати бібліотеки, щоб впровадити цей потужний функціональний патерн у свої проєкти задовго до того, як він стане нативною функцією мови.
Що таке зіставлення зі зразком? Виходимо за межі операторів switch
За своєю суттю, зіставлення зі зразком — це процес деконструкції структур даних, щоб перевірити, чи відповідають вони певному 'зразку' або формі. Якщо збіг знайдено, ми можемо виконати пов'язаний блок коду, часто прив'язуючи частини зіставлених даних до локальних змінних для використання в цьому блоці.
Порівняймо це з традиційним оператором switch. Оператор switch обмежується перевірками на строгу рівність (===) для одного значення:
function getHttpStatusMessage(status) {
switch (status) {
case 200:
return 'OK';
case 404:
return 'Not Found';
case 500:
return 'Internal Server Error';
default:
return 'Unknown Status';
}
}
Це чудово працює для простих примітивних значень. Але що, якби ми хотіли обробити складніший об'єкт, наприклад, відповідь API?
const response = { status: 'success', data: { user: 'John Doe' } };
// або
const errorResponse = { status: 'error', error: { code: 'E401', message: 'Unauthorized' } };
Оператор switch не може елегантно впоратися з цим. Ви були б змушені використовувати безладну серію операторів if/else, перевіряючи наявність властивостей та їхні значення. Саме тут зіставлення зі зразком проявляє себе найкраще. Воно може перевірити всю форму об'єкта.
Підхід із зіставленням зі зразком концептуально виглядав би так (використовуючи гіпотетичний майбутній синтаксис):
function handleResponse(response) {
return match (response) {
when { status: 'success', data: d }: `Success! Data received for ${d.user}`,
when { status: 'error', error: e }: `Error ${e.code}: ${e.message}`,
default: 'Invalid response format'
}
}
Зверніть увагу на ключові відмінності:
- Структурне зіставлення: Воно порівнює зі структурою об'єкта, а не лише з одним значенням.
- Прив'язка даних: Воно витягує вкладені значення (наприклад, `d` та `e`) безпосередньо в зразку.
- Орієнтація на вирази: Весь блок `match` є виразом, що повертає значення, усуваючи потребу в тимчасових змінних та операторах `return` у кожній гілці. Це ключовий принцип функціонального програмування.
Стан зіставлення зі зразком у JavaScript
Важливо встановити чіткі очікування для глобальної аудиторії розробників: зіставлення зі зразком ще не є стандартною, нативною функцією JavaScript.
Існує активна пропозиція TC39 щодо додавання цієї функції до стандарту ECMAScript. Однак на момент написання статті вона перебуває на Етапі 1, що означає, що вона знаходиться на ранній стадії дослідження. Ймовірно, мине кілька років, перш ніж ми побачимо її нативну реалізацію у всіх основних браузерах та середовищах Node.js.
Отже, як ми можемо використовувати її сьогодні? Ми можемо покластися на жваву екосистему JavaScript. Було розроблено кілька чудових бібліотек, щоб привнести потужність зіставлення зі зразком у сучасний JavaScript та TypeScript. Для прикладів у цій статті ми будемо переважно використовувати ts-pattern — популярну та потужну бібліотеку, яка є повністю типізованою, дуже виразною та бездоганно працює як у TypeScript, так і в звичайних JavaScript-проєктах.
Основні концепції функціонального зіставлення зі зразком
Давайте заглибимося в фундаментальні патерни, з якими ви зіткнетеся. Ми будемо використовувати ts-pattern для наших прикладів коду, але концепції є універсальними для більшості реалізацій зіставлення зі зразком.
Літеральні зразки: найпростіше зіставлення
Це найпростіша форма зіставлення, схожа на `case` в операторі `switch`. Вона порівнює з примітивними значеннями, такими як рядки, числа, булеві значення, `null` та `undefined`.
import { match } from 'ts-pattern';
function getPaymentMethod(method) {
return match(method)
.with('credit_card', () => 'Processing with Credit Card Gateway')
.with('paypal', () => 'Redirecting to PayPal')
.with('crypto', () => 'Processing with Cryptocurrency Wallet')
.otherwise(() => 'Invalid Payment Method');
}
console.log(getPaymentMethod('paypal')); // "Redirecting to PayPal"
console.log(getPaymentMethod('bank_transfer')); // "Invalid Payment Method"
Синтаксис .with(pattern, handler) є центральним. Клауза .otherwise() є еквівалентом `default` і часто необхідна для забезпечення вичерпності зіставлення (обробки всіх можливих варіантів).
Деструктуруючі зразки: розпакування об'єктів та масивів
Саме тут зіставлення зі зразком справді виділяється. Ви можете зіставляти зі структурою та властивостями об'єктів і масивів.
Деструктуризація об'єктів:
Уявіть, що ви обробляєте події в додатку. Кожна подія — це об'єкт з `type` та `payload`.
import { match, P } from 'ts-pattern'; // P — це об'єкт-заповнювач
function handleEvent(event) {
return match(event)
.with({ type: 'USER_LOGIN', payload: { userId: P.select() } }, (userId) => {
console.log(`User ${userId} logged in.`);
// ... викликати побічні ефекти входу
})
.with({ type: 'ADD_TO_CART', payload: { productId: P.select('id'), quantity: P.select('qty') } }, ({ id, qty }) => {
console.log(`Added ${qty} of product ${id} to the cart.`);
})
.with({ type: 'PAGE_VIEW' }, () => {
console.log('Page view tracked.');
})
.otherwise(() => {
console.log('Unknown event received.');
});
}
handleEvent({ type: 'USER_LOGIN', payload: { userId: 'u-123', timestamp: 1678886400 } });
handleEvent({ type: 'ADD_TO_CART', payload: { productId: 'prod-abc', quantity: 2 } });
У цьому прикладі P.select() є потужним інструментом. Він діє як wildcard, що відповідає будь-якому значенню на цій позиції та прив'язує його, роблячи доступним для функції-обробника. Ви навіть можете назвати вибрані значення для більш описової сигнатури обробника.
Деструктуризація масивів:
Ви також можете зіставляти зі структурою масивів, що неймовірно корисно для таких завдань, як парсинг аргументів командного рядка або робота з кортежеподібними даними.
function parseCommand(args) {
return match(args)
.with(['install', P.select()], (pkg) => `Installing package: ${pkg}`)
.with(['delete', P.select(), '--force'], (file) => `Force deleting file: ${file}`)
.with(['list'], () => 'Listing all items...')
.with([], () => 'No command provided. Use --help for options.')
.otherwise((unrecognized) => `Error: Unrecognized command sequence: ${unrecognized.join(' ')}`);
}
console.log(parseCommand(['install', 'react'])); // "Installing package: react"
console.log(parseCommand(['delete', 'temp.log', '--force'])); // "Force deleting file: temp.log"
console.log(parseCommand([])); // "No command provided..."
Wildcard та зразки-заповнювачі
Ми вже бачили P.select(), заповнювач, що прив'язує значення. ts-pattern також надає простий wildcard, P._, для випадків, коли вам потрібно зіставити позицію, але вас не хвилює її значення.
P._(Wildcard): Відповідає будь-якому значенню, але не прив'язує його. Використовуйте його, коли значення має існувати, але ви не будете його використовувати.P.select()(Заповнювач): Відповідає будь-якому значенню та прив'язує його для використання в обробнику.
match(data)
.with(['SUCCESS', P._, P.select()], (message) => `Success with message: ${message}`)
// Тут ми ігноруємо другий елемент, але захоплюємо третій.
.otherwise(() => 'No success message');
Захисні умови: додавання умовної логіки з .when()
Іноді зіставлення форми недостатньо. Можливо, вам знадобиться додати додаткову умову. Саме тут на допомогу приходять захисні умови (guard clauses). У ts-pattern це досягається за допомогою методу .when() або предиката P.when().
Уявіть, що ви обробляєте замовлення. Ви хочете по-різному обробляти замовлення з високою вартістю.
function getOrderStatus(order) {
return match(order)
.with({ status: 'shipped', total: P.when(t => t > 1000) }, () => 'High-value order shipped.')
.with({ status: 'shipped' }, () => 'Standard order shipped.')
.with({ status: 'processing', items: P.when(items => items.length === 0) }, () => 'Warning: Processing empty order.')
.with({ status: 'processing' }, () => 'Order is being processed.')
.with({ status: 'cancelled' }, () => 'Order has been cancelled.')
.otherwise(() => 'Unknown order status.');
}
console.log(getOrderStatus({ status: 'shipped', total: 1500 })); // "High-value order shipped."
console.log(getOrderStatus({ status: 'shipped', total: 50 })); // "Standard order shipped."
console.log(getOrderStatus({ status: 'processing', items: [] })); // "Warning: Processing empty order."
Зверніть увагу, що більш специфічний зразок (із захисною умовою .when()) має йти перед більш загальним. Перемагає перший зразок, який успішно зіставився.
Типові та предикативні зразки
Ви також можете зіставляти з типами даних або власними предикативними функціями, що забезпечує ще більшу гнучкість.
function describeValue(x) {
return match(x)
.with(P.string, () => 'This is a string.')
.with(P.number, () => 'This is a number.')
.with({ message: P.string }, () => 'This is an error object.')
.with(P.instanceOf(Date), (d) => `This is a Date object for ${d.getFullYear()}.`)
.otherwise(() => 'This is some other type of value.');
}
Практичні випадки використання в сучасній веброзробці
Теорія — це чудово, але давайте подивимося, як зіставлення зі зразком вирішує реальні проблеми для глобальної аудиторії розробників.
Обробка складних відповідей API
Це класичний випадок використання. API рідко повертають єдину, фіксовану структуру. Вони повертають об'єкти успіху, різноманітні об'єкти помилок або стани завантаження. Зіставлення зі зразком чудово це впорядковує.
Error: The requested resource was not found. An unexpected error occurred: ${err.message}// Припустимо, це стан з хука для отримання даних
const apiState = { status: 'error', error: { code: 403, message: 'Forbidden' } };
function renderUI(state) {
return match(state)
.with({ status: 'loading' }, () => '
.with({ status: 'success', data: P.select() }, (users) => `${users.map(u => `
`)
.with({ status: 'error', error: { code: 404 } }, () => '
.with({ status: 'error', error: P.select() }, (err) => `
.exhaustive(); // Гарантує, що всі випадки нашого типу стану оброблені
}
// document.body.innerHTML = renderUI(apiState);
Це набагато читабельніше та надійніше, ніж вкладені перевірки `if (state.status === 'success')`.
Управління станом у функціональних компонентах (наприклад, React)
У бібліотеках управління станом, таких як Redux, або при використанні хука `useReducer` в React, у вас часто є функція-редюсер, яка обробляє різні типи дій. Використання `switch` за `action.type` є поширеним, але зіставлення зі зразком для всього об'єкта `action` є кращим.
// До: типовий редюсер з оператором switch
function classicReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_VALUE':
return { ...state, count: action.payload };
default:
return state;
}
}
// Після: редюсер, що використовує зіставлення зі зразком
function patternMatchingReducer(state, action) {
return match(action)
.with({ type: 'INCREMENT' }, () => ({ ...state, count: state.count + 1 }))
.with({ type: 'DECREMENT' }, () => ({ ...state, count: state.count - 1 }))
.with({ type: 'SET_VALUE', payload: P.select() }, (value) => ({ ...state, count: value }))
.otherwise(() => state);
}
Версія зі зіставленням зі зразком є більш декларативною. Вона також запобігає поширеним помилкам, таким як доступ до `action.payload`, коли він може не існувати для певного типу дії. Сам зразок гарантує, що `payload` має існувати для випадку `'SET_VALUE'`.
Реалізація скінченних автоматів (FSM)
Скінченний автомат — це модель обчислень, яка може перебувати в одному з скінченної кількості станів. Зіставлення зі зразком є ідеальним інструментом для визначення переходів між цими станами.
// Стани: { status: 'idle' } | { status: 'loading' } | { status: 'success', data: T } | { status: 'error', error: E }
// Події: { type: 'FETCH' } | { type: 'RESOLVE', data: T } | { type: 'REJECT', error: E }
function stateMachine(currentState, event) {
return match([currentState, event])
.with([{ status: 'idle' }, { type: 'FETCH' }], () => ({ status: 'loading' }))
.with([{ status: 'loading' }, { type: 'RESOLVE', data: P.select() }], (data) => ({ status: 'success', data }))
.with([{ status: 'loading' }, { type: 'REJECT', error: P.select() }], (error) => ({ status: 'error', error }))
.with([{ status: 'error' }, { type: 'FETCH' }], () => ({ status: 'loading' }))
.otherwise(() => currentState); // Для всіх інших комбінацій залишатися в поточному стані
}
Цей підхід робить валідні переходи між станами явними та легкими для розуміння.
Переваги для якості та підтримки коду
Впровадження зіставлення зі зразком — це не просто написання розумного коду; воно має відчутні переваги для всього життєвого циклу розробки програмного забезпечення.
- Читабельність та декларативний стиль: Зіставлення зі зразком змушує вас описувати, як виглядають ваші дані, а не імперативні кроки для їх перевірки. Це робить намір вашого коду зрозумілішим для інших розробників, незалежно від їхнього культурного чи мовного походження.
- Імутабельність та чисті функції: Орієнтована на вирази природа зіставлення зі зразком ідеально поєднується з принципами функціонального програмування. Вона заохочує вас брати дані, трансформувати їх і повертати нове значення, а не мутувати стан напряму. Це призводить до меншої кількості побічних ефектів та більш передбачуваного коду.
- Перевірка на вичерпність: Це кардинально змінює надійність. При використанні TypeScript, бібліотеки, такі як `ts-pattern`, можуть на етапі компіляції змусити вас обробити кожен можливий варіант об'єднаного типу (union type). Якщо ви додаєте новий стан або тип дії, компілятор видасть помилку, доки ви не додасте відповідний обробник у вашому виразі зіставлення. Ця проста функція усуває цілий клас помилок під час виконання.
- Зниження цикломатичної складності: Це вирівнює глибоко вкладені структури `if/else` в єдиний, лінійний та легкий для читання блок. Код з нижчою складністю легше тестувати, налагоджувати та підтримувати.
Як почати використовувати зіставлення зі зразком сьогодні
Готові спробувати? Ось простий, дієвий план:
- Виберіть свій інструмент: Ми настійно рекомендуємо
ts-patternза його надійний набір функцій та чудову підтримку TypeScript. Це золотий стандарт в екосистемі JavaScript сьогодні. - Встановлення: Додайте його до свого проєкту за допомогою обраного вами пакетного менеджера.
npm install ts-pattern
абоyarn add ts-pattern - Рефакторинг невеликого шматка коду: Найкращий спосіб навчитися — це практика. Знайдіть складний оператор `switch` або безладний ланцюжок `if/else` у вашій кодовій базі. Це може бути компонент, який рендерить різний UI на основі пропсів, функція, що парсить дані API, або редюсер. Спробуйте його відрефакторити.
Примітка щодо продуктивності
Поширене питання — чи несе використання бібліотеки для зіставлення зі зразком штраф за продуктивність. Відповідь — так, але він майже завжди незначний. Ці бібліотеки високо оптимізовані, і накладні витрати мізерні для переважної більшості вебдодатків. Величезні переваги у продуктивності розробників, ясності коду та запобіганні помилок значно перевершують вартість продуктивності на рівні мікросекунд. Не оптимізуйте передчасно; надавайте пріоритет написанню чистого, коректного та підтримуваного коду.
Майбутнє: нативне зіставлення зі зразком в ECMAScript
Як уже згадувалося, комітет TC39 працює над додаванням зіставлення зі зразком як нативної функції. Синтаксис все ще обговорюється, але він може виглядати приблизно так:
// Можливий майбутній синтаксис!
let httpMessage = match (response) {
when { status: 200, body: b } -> `Success with body: ${b}`,
when { status: 404 } -> `Not Found`,
when { status: 5.. } -> `Server Error`,
else -> `Other HTTP response`
};
Вивчаючи концепції та патерни сьогодні за допомогою бібліотек, таких як ts-pattern, ви не просто покращуєте свої поточні проєкти; ви готуєтеся до майбутнього мови JavaScript. Ментальні моделі, які ви побудуєте, безпосередньо перенесуться, коли ці функції стануть нативними.
Висновок: зміна парадигми для умовних конструкцій у JavaScript
Зіставлення зі зразком — це набагато більше, ніж синтаксичний цукор для оператора switch. Воно являє собою фундаментальний зсув у бік більш декларативного, надійного та функціонального стилю обробки умовної логіки в JavaScript. Воно заохочує вас думати про форму ваших даних, що призводить до коду, який не тільки елегантніший, але й більш стійкий до помилок та легший у підтримці з часом.
Для команд розробників по всьому світу впровадження зіставлення зі зразком може призвести до більш послідовної та виразної кодової бази. Воно надає спільну мову для обробки складних структур даних, яка виходить за межі простих перевірок наших традиційних інструментів. Ми заохочуємо вас дослідити його у вашому наступному проєкті. Почніть з малого, відрефакторте складну функцію та відчуйте ясність і потужність, які воно приносить у ваш код.